package com.ecommerce.service;

import com.ecommerce.common.es.PitManager;
import com.ecommerce.dto.ProductQueryRequestDTO;
import com.ecommerce.dto.ProductQueryResponseDTO;
import com.ecommerce.product.domain.Product;
import com.ecommerce.product.enums.ProductType;
import com.ecommerce.product.domain.IndexNameResolver;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.PointInTimeBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 商品搜索服务 - 使用PIT(Point-In-Time)实现安全分页，
 * 此类在查询ES库时使用 Elasticsearch Java Client (RestHighLevelClient，底层原生API)，
 * 而不是 Spring Data Elasticsearch (elasticsearchRestTemplate，Spring Data 封装)，
 * <p>
 * 本服务提供基于Elasticsearch PIT机制的安全分页查询功能，
 * 确保在分页过程中数据一致性不受实时更新的影响。
 * </p>
 */
@Slf4j
@Service
public class ProductPageQueryPitService {

    /**
     * Elasticsearch高级客户端
     */
    private final RestHighLevelClient esClient;

    /**
     * 索引名称解析器
     */
    private final IndexNameResolver indexNameResolver;

    /**
     * 默认每页大小
     */
    private final int defaultPageSize;

    /**
     * PIT保持时间
     * <p>
     * 表示Point-In-Time快照的存活时间
     * </p>
     */
    private final TimeValue pitKeepAlive;

    /**
     * PIT续期保持时间（支持时间单位）
     * <p>
     * 配置格式如："5m"表示5分钟
     * </p>
     */
    @Value("${app.elasticsearch.search.pit-extend-keep-alive:5m}")
    private String pitExtendKeepAlive;

    /**
     * 构造函数
     * @param esClient Elasticsearch高级客户端
     * @param indexNameResolver 索引名称解析器
     * @param defaultPageSize 默认分页大小
     * @param pitKeepAlive PIT存活时间配置
     */
    @Autowired
    public ProductPageQueryPitService(
            RestHighLevelClient esClient,
            IndexNameResolver indexNameResolver,
            @Value("${app.elasticsearch.search.default-page-size:20}") int defaultPageSize,
            @Value("${app.elasticsearch.search.pit-keep-alive:5m}") String pitKeepAlive) {
        this.esClient = esClient;
        this.indexNameResolver = indexNameResolver;
        this.defaultPageSize = defaultPageSize;
        this.pitKeepAlive = TimeValue.parseTimeValue(pitKeepAlive, null, "pitKeepAlive");
    }

    /**
     * 分页搜索商品，每次只返回1页数据给前端，使用PIT和search_after。
     * @param request 查询请求DTO
     * @param productType 商品类型
     * @return 分页响应DTO
     */
    public ProductQueryResponseDTO searchProducts(ProductQueryRequestDTO request, ProductType productType) {
        try {
            // 解析索引名称
            String indexName = indexNameResolver.resolveIndexName(productType);

            // 确定分页大小
            int pageSize = request.getPageSize() != null ? request.getPageSize() : defaultPageSize;

            if (request.getPitId() == null) {
                //初始查询（会创建PIT）
                return initialSearch(request, indexName, pageSize);
            } else {
                //续期PIT（防止超时失效），若已超时则根据索引名称重新创建PIT,避免因PIT超时中断用户体验
                String newPitId = extendOrRecreatePit(request.getPitId(), indexName);
                request.setPitId(newPitId);

                //后续分页查询（不再需要索引名，因为 pitId 已隐含了索引信息）
                return continueSearch(request, pageSize);
            }
        } catch (Exception e) {
            log.error("商品搜索失败", e);
            // 返回空响应而不是抛出异常，保证前端体验
            return new ProductQueryResponseDTO();
        }
    }

    /**
     * 初始查询（创建PIT）
     * @param request 查询请求
     * @param indexName 索引名称
     * @param pageSize 分页大小
     * @return 查询响应
     * @throws IOException 当ES操作失败时抛出
     */
    private ProductQueryResponseDTO initialSearch(ProductQueryRequestDTO request, String indexName, int pageSize)
            throws IOException {
        // 1. 创建PIT
        String pitId = createPit(indexName);

        try {
            // 2. 构建查询请求
            SearchRequest searchRequest = buildSearchRequest(request, pageSize, pitId, null);

            // 3. 执行查询
            SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);

            // 4. 构建响应
            return buildResponse(response, pitId);
        } catch (Exception e) {
            // 创建失败时清理PIT资源
            closePit(pitId);
            throw e;
        }
    }

    /**
     * 后续分页查询
     * <p>
     * 相关知识：
     * 后续分页查询不需要索引名，Elasticsearch可通过pitId定位到：
     * 原始索引的快照、查询时的数据一致性视图（不受后续数据变更影响），
     * 不再需要索引名，是因为 pitId 已隐含了索引信息。
     * <p>
     * 跨索引查询：
     * 如果初始查询涉及多个索引（如 product_* 通配符），PIT会覆盖所有匹配的索引，后续分页仍无需指定索引名。
     * </p>
     * @param request 查询请求
     * @param pageSize 分页大小
     * @return 查询响应
     * @throws IOException 当ES操作失败时抛出
     */
    private ProductQueryResponseDTO continueSearch(ProductQueryRequestDTO request, int pageSize)
            throws IOException {
        try {
            // 1. 构建查询请求
            SearchRequest searchRequest = buildSearchRequest(
                    request, pageSize, request.getPitId(), request.getSearchAfter());

            // 2. 执行查询
            SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);

            // 3. 构建响应
            return buildResponse(response, request.getPitId());
        } catch (Exception e) {
            // 查询失败时清理PIT资源
            closePit(request.getPitId());
            throw e;
        }
    }

    /**
     * 创建PIT(Point-In-Time)
     * @param indexName 索引名称
     * @return PIT ID
     * @throws IOException 当创建失败时抛出
     */
    private String createPit(String indexName) throws IOException {
        OpenPointInTimeRequest pitRequest = new OpenPointInTimeRequest(indexName)
                .keepAlive(pitKeepAlive);
        OpenPointInTimeResponse response = esClient.openPointInTime(pitRequest, RequestOptions.DEFAULT);
        return response.getPointInTimeId();
    }

    /**
     * 构建搜索请求
     * @param request 查询请求DTO
     * @param pageSize 分页大小
     * @param pitId PIT ID
     * @param searchAfter 分页标记
     * @return 构建好的搜索请求
     */
    private SearchRequest buildSearchRequest(
            ProductQueryRequestDTO request,
            int pageSize,
            String pitId,
            Object[] searchAfter) {

        // 构建基础查询条件
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                .must(QueryBuilders.termQuery("status", 1)); // 只查询上架商品

        // 添加分类条件
        if (request.getPrimaryCategory() != null) {
            boolQuery.must(QueryBuilders.termQuery("categoryId", request.getPrimaryCategory()));
        }
        if (request.getSecondaryCategory() != null) {
            boolQuery.must(QueryBuilders.termQuery("subCategoryId", request.getSecondaryCategory()));
        }

        // 添加关键词查询
        if (request.getKeyword() != null && !request.getKeyword().isEmpty()) {
            boolQuery.must(QueryBuilders.matchQuery("name", request.getKeyword()));
        }

        // 构建搜索源
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
                .query(boolQuery)
                .size(pageSize)
                .sort("_score", SortOrder.DESC)
                .sort("id", SortOrder.ASC) // 确保排序唯一性
                .pointInTimeBuilder(new PointInTimeBuilder(pitId));

        // 设置分页标记
        if (searchAfter != null) {
            sourceBuilder.searchAfter(searchAfter);
        }

        return new SearchRequest().source(sourceBuilder);
    }

    /**
     * 构建响应DTO
     * @param response ES查询响应
     * @param pitId PIT ID
     * @return 构建好的响应DTO
     */
    private ProductQueryResponseDTO buildResponse(SearchResponse response, String pitId) {
        ProductQueryResponseDTO result = new ProductQueryResponseDTO();
        result.setPitId(pitId);
        result.setTotalHits(response.getHits().getTotalHits().value);

        // 转换商品列表
        List<Product> products = Arrays.stream(response.getHits().getHits())
                .map(this::convertToProduct)
                .collect(Collectors.toList());
        result.setProducts(products);

        // 设置下一页的searchAfter参数
        if (response.getHits().getHits().length > 0) {
            SearchHit lastHit = response.getHits().getHits()[response.getHits().getHits().length - 1];
            result.setSearchAfter(lastHit.getSortValues());
        }

        return result;
    }

    /**
     * 转换SearchHit为Product对象
     * @param hit ES查询结果中的hit对象
     * @return 转换后的Product对象
     */
    private Product convertToProduct(SearchHit hit) {
        Product product = new Product();
        product.setId(Long.parseLong(hit.getId()));
        product.setName((String) hit.getSourceAsMap().get("name"));
        // 其他字段转换...
        return product;
    }

    /**
     * 续期 PIT,若已超时则根据索引名称重新创建PIT
     * @param pitId 要续期的PIT ID
     * @param indexName 索引名称
     * @return 新的PIT ID
     * @throws IOException 当操作失败时抛出
     */
    private String extendOrRecreatePit(String pitId, String indexName) throws IOException {
        String newPitId = PitManager.extendPit(
                esClient.getLowLevelClient(),
                pitId,
                pitExtendKeepAlive
        );

        if (newPitId == null) {
            // 重新创建PIT
            return createPit(indexName);
        }

        return newPitId;
    }

    /**
     * 关闭PIT资源
     * @param pitId 要关闭的PIT ID
     */
    public void closePit(String pitId) {
        if (pitId == null) return;

        try {
            ClosePointInTimeRequest request = new ClosePointInTimeRequest(pitId);
            esClient.closePointInTime(request, RequestOptions.DEFAULT);
            log.info("成功关闭PIT: {}", pitId);
        } catch (Exception e) {
            log.error("关闭PIT失败: {}", pitId, e);
        }
    }
}